package np13.monads;

import java.util.LinkedList;

import np13.Slime;
import np13.SlimeQueue;
import np13.Smell;
import np13.Snowden;
import np13.World;
import np13.earth.Funafitu;
import np13.earth.HomeLand;
import np13.ferry.Ferry;
import np13.sddb.Result;
import np13.sddb.SDDB;
import np13.sddb.queries.AddSmellResultQuery;
import np13.sddb.queries.GetRecordQuery;

/**
 * This class represents a staff member on the {@link Funafitu}.
 * 
 * Each instance has multiple duties for his {@link Smell}: Sending slimes from
 * the {@link SlimeQueue} to the {@link HomeLand} and redistributing the
 * returners by sending them to other {@link SlimeQueue}s. The
 * {@link FunafituMonad} needs to inform the {@link Snowden} about certain
 * events.
 */
public class FunafituMonad {

	/**
	 * The {@link Smell} of the {@link HomeLand} where the slimes are sent to by
	 * the monad.
	 */
	private final Smell smell;

	/**
	 * A reference to the {@link Funafitu} atoll.
	 */
	private final Funafitu funafitu;

	/**
	 * The {@link SlimeQueue} the {@link FunafituMonad} is responsible for.
	 */
	private final SlimeQueue myQueue;

	/**
	 * A reference to the one and only {@link Snowden}.
	 */
	private final Snowden snowden;

	/**
	 * A reference to the {@link SDDB}.
	 */
	private final SDDB sddb;

	/**
	 * True if the {@link FunafituMonad} is started, false otherwise.
	 */
	private boolean started = false;

	/**
	 * This flag is set by {@link Snowden} to singal the last trip.
	 */
	private boolean lastTrip = false;

	/**
	 * This flag is set to true by {@link Snowden} to singal that some other
	 * {@link SlimeQueue} needs to be filled up. Gets reset after loading that
	 * other Queue is done.
	 */
	private boolean loadOther = false;
	/**
	 * Counts how many times loadOther has been called by {@link Snowden}. In
	 * private method loadOther is beeing waited until the public one has been
	 * called twice and hits counter equals 2. Gets reset after loading that
	 * other Queue is done.
	 */
	private int loadOtherCount = 0;
	/**
	 * The {@link SlimeQueue} that should be filled up.
	 */
	private SlimeQueue otherQueue = null;
	/**
	 * The {@link Smell} of the {@link SlimeQueue} that should be filled up.
	 */
	private Smell otherSmell = null;
	/**
	 * This flag represents that {@link FunafituMonad}s own {@link SlimeQueue}
	 * is not being filled up, and more importantly that once it is being and
	 * gets filled up, it loads own {@link Ferry} before it may start to load
	 * some other {@link SlimeQueue}.
	 */
	private boolean mayLoadOther = true;

	/**
	 * Initializes the Funafitu habitat.
	 * 
	 * @param smell
	 *            The smell of the homeland the monad is responsible for.
	 */
	public FunafituMonad(Smell smell) {
		this.smell = smell;
		funafitu = World.getInstance().getFunafitu();
		myQueue = funafitu.getQueues().get(smell);
		snowden = World.getInstance().getSnowden();
		sddb = World.getInstance().getsDDB();
	}

	/**
	 * This method empties the returned {@link Ferry} by pushing the returned
	 * Slimes onto other {@link SlimeQueue}s according to {@link Snowden}s
	 * advice.
	 */
	private void emptyFerry() {
		try {
			// get the ferry once it docks on to our pier
			Ferry myFerry = myQueue.getPier().getPieredFerry();
			System.out.println(Thread.currentThread().getName() + " returned "
					+ myFerry.getSize());
			// run over all slimes in the ferry. Add their smell into SDDB and
			// get their Record.
			Slime slime = null;
			Result result;
			LinkedList<Smell> advice;
			while (myFerry.getSize() > 0) {
				if (slime == null) {
					slime = myFerry.popSlime();
				}
				sddb.putQuery(new AddSmellResultQuery(slime, smell));
				sddb.putQuery(new GetRecordQuery(slime));
				result = sddb.getResultForSlime(slime);
				// check if loading a specific queue
				synchronized (this) {
					if (loadOther && !(result.getRecord().contains(otherSmell)))
						if (otherQueue.loadSlime(slime)) {
							slime = null;
							continue;

						} else {
							loadOther = false;
							loadOtherCount = 0;
							snowden.doneLoad();
						}
				}
				// if not filling a specific queue or the slime was already
				// there, then get Snowdens advice and push on some other queue
				advice = snowden.advice();
				for (Smell s : advice) {
					synchronized (this) {
						// check again the condition to assure that the
						// environment didn't change between the two
						// synchronized blocks
						if (!loadOther
								|| result.getRecord().contains(otherSmell)) {
							if (result.getRecord().contains(s))
								continue;
							else {
								funafitu.getQueues().get(s).pushSlime(slime);
								slime = null;
								break;
							}
						} else {
							System.out
									.println(Thread.currentThread().getName()
											+ " rechecking the destination of returner.");
							break;
						}
					}
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Performs the tasks the {@link FunafituMonad} has to do. Must be called in
	 * a separate thread.
	 * 
	 * @throws InterruptedException
	 */
	private void eventLoop() throws InterruptedException {
		System.out.println("starting funafituMonad Thread ");
		System.out.println("starting to work");
		boolean loadO;
		while (started) {
			System.out.println(Thread.currentThread().getName() + " "
					+ myQueue.getCurrentSize());
			System.out.println(Thread.currentThread().getName() + " "
					+ myQueue.getTargetSize());
			synchronized (this) {
				// wait until there is something to do
				while ((myQueue.getCurrentSize() < myQueue.getTargetSize())
						&& !lastTrip && !loadOther) {
					// inform Snowden that own queue is not full
					snowden.needMore(smell);
					System.out.println(Thread.currentThread().getName()
							+ " is waiting for work");
					wait();
				}
			}
			// if some other queue should be filled up and own queue didn't just
			// got filled up, then first fill up that other queue.
			synchronized (this) {
				loadO = loadOther && mayLoadOther;
			}
			if (loadO)
				loadOther();
			else {
				loadFerry();
				emptyFerry();
				if (lastTrip)
					assert (myQueue.isEmpy());
			}
		}
		System.out.println(Thread.currentThread().getName()
				+ " --->IS ENDING!!<---");
	}

	/**
	 * @return The smell of the homeland the monad is responsible for.
	 */
	public Smell getSmell() {
		return smell;
	}

	/**
	 * This method is called by {@link Snowden} announcing the last trip for
	 * this {@link Smell} line.
	 */
	public synchronized void lastTrip() {
		lastTrip = true;
		notifyAll();
	}

	/**
	 * This method loads the {@link Ferry} and sends it sailing.
	 */
	private void loadFerry() {
		synchronized (this) {
			// set that after this method other queues may be filled up if
			// necessary
			mayLoadOther = true;
		}
		try {
			Ferry myFerry = myQueue.getPier().getPieredFerry();
			int amountToLoad;
			// decide the amount of Slimes to load, depending on if it is the
			// last trip or not
			synchronized (this) {
				if (lastTrip) {
					System.out.println(Thread.currentThread().getName()
							+ " ON THE LAAASSSSTTTT  TRRRIIIPPPP!!!");
					started = false;
					amountToLoad = myQueue.getCurrentSize();
				} else
					amountToLoad = myQueue.getTargetSize();
			}
			System.out.println(Thread.currentThread().getName()
					+ " loading ferry with " + amountToLoad);
			for (int i = 0; i < amountToLoad; i++) {
				myFerry.pushSlime(myQueue.dequeueSlime());
			}
			myFerry.sail();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	/**
	 * This method loads some other {@link SlimeQueue}, once the corresponding
	 * flags have been set by {@link Snowden}. It first waits for
	 * {@link Snowden} to call the public loadOther method for the second time,
	 * to insure that on every {@link FunafituMonad} the same method has been
	 * called at least once.
	 */
	private void loadOther() {
		synchronized (this) {
			while (loadOtherCount != 2)
				try {
					wait();
				} catch (InterruptedException e1) {
					e1.printStackTrace();
				}
		}
		System.out.println(Thread.currentThread().getName() + " loading "
				+ otherSmell.name());
		Slime slime;
		Result result = null;
		// mark last slime in own queue
		Slime last = myQueue.peekLastSlime();
		// check if there is any slimes in own Queue
		boolean run = last != null;
		// look in own queue first
		while (run) {
			slime = myQueue.dequeueSlime();
			sddb.putQuery(new GetRecordQuery(slime));
			try {
				result = sddb.getResultForSlime(slime);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// if a slime already was in other queue, then put it back at the
			// end of own queue, else try to put it in the other queue
			if (result.getRecord().contains(otherSmell))
				myQueue.enqueueSlime(slime);
			else if (!otherQueue.loadSlime(slime)) {
				myQueue.enqueueSlime(slime);
				synchronized (this) {
					// if can't load other anymore, inform Snowden and reset the
					// flags for loading other queue
					snowden.doneLoad();
					loadOther = false;
					loadOtherCount = 0;
				}
				return;
			}
			// if reached the slime wich was marked as last, stop looking in own
			// queue and continue by looking in the ferry
			if (slime.equals(last)) {
				run = false;
			}
		}
		try {
			Ferry myFerry = myQueue.getPier().getPieredFerry();
			boolean loadOth = true;
			// run over all slimes in the ferry and check if they can be pull
			// into the other queue
			while (myFerry.getSize() > 0) {
				slime = myFerry.popSlime();
				sddb.putQuery(new AddSmellResultQuery(slime, smell));
				sddb.putQuery(new GetRecordQuery(slime));
				result = sddb.getResultForSlime(slime);
				// check if should still load other, and if the slime was
				// already there
				if (loadOth && !(result.getRecord().contains(otherSmell))) {
					if (!otherQueue.loadSlime(slime)) {
						loadOth = false;
						synchronized (this) {
							// if can't load other anymore, inform Snowden and
							// reset the flags for loading other queue
							snowden.doneLoad();
							loadOther = false;
							loadOtherCount = 0;
						}
					} else
						continue;
				} else {
					// otherwise distribute by advising Snowden
					for (Smell s : snowden.advice()) {
						if (result.getRecord().contains(s))
							continue;
						else {
							funafitu.getQueues().get(s).pushSlime(slime);
							break;
						}
					}
				}
			}
			// at last inform Snowden that done loading other, and reset the
			// load other flags
			if (loadOth) {
				synchronized (this) {
					snowden.doneLoad();
					loadOther = false;
					loadOtherCount = 0;
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

	}

	/**
	 * This method is call by {@link Snowden} once some other {@link SlimeQueue}
	 * needs to be filled up. It sets the corresponding flags and notifies
	 * {@link FunafituMonad}.
	 * 
	 * @param other
	 *            {@link Smell} of the {@link SlimeQueue}, which is to be filled
	 *            up.
	 * 
	 * @return if flags have been set during the {@link FunafituMonad} is still
	 *         working.
	 */
	public synchronized boolean loadOther(Smell other) {
		if (!started)
			return false;
		loadOther = true;
		loadOtherCount++;
		otherSmell = other;
		otherQueue = funafitu.getQueues().get(other);
		notifyAll();
		return true;
	}

	/**
	 * This method is called by {@link Snowden} once other {@link FunafituMonad}
	 * s are done with filling up the {@link SlimeQueue} of this
	 * {@link FunafituMonad}. It sets the mayLoadOther flag on false, to insure
	 * loading of the own {@link Ferry} before starting to load other
	 * {@link SlimeQueue}. If there are less slimes in the {@link SlimeQueue}
	 * then the capacity of the {@link Ferry}, then is lastTrip flag set to
	 * true, in order to turn on the highly toxic regenerative energy source. At
	 * the end the {@link FunafituMonad} is notified.
	 */
	public synchronized void mayLoad() {
		mayLoadOther = false;
		if (myQueue.getCurrentSize() < myQueue.getTargetSize())
			lastTrip = true;
		notifyAll();
	}

	/**
	 * A helper method for setting thread names.
	 * 
	 * @return {@link String} containing a multiplicative of five tabs,
	 *         depending on the {@link Smell}.
	 */
	private String smell() {
		switch (smell) {
		case VATIKAN:
			return "\t\t\t\t\t*";
		case SCHWARZRUSSLAND:
			return "\t\t\t\t\t\t\t\t\t\t*";
		case MIKRONESIEN:
			return "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t*";
		case MAKRONESIEN:
			return "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t*";
		}
		return null;
	}

	/**
	 * Starts the monad. May be called only once.
	 */
	public void start() {
		// myQueue.setMonad(this);
		synchronized (this) {
			if (started)
				throw new IllegalStateException();
			started = true;
		}
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					eventLoop();
					snowden.monadDone();
				} catch (InterruptedException e) {
				}
			}
		});
		thread.setName(smell() + smell.name() + "*");
		System.out.println(thread.getName() + " is a Funafitu Monad!!");

		thread.start();
	}
}
